package de.sendorian.app.forumArchive.finder;

import static de.sendorian.util.matcher.FileTypeMatchers.archive;
import static org.apache.commons.lang.StringEscapeUtils.unescapeHtml;
import static org.apache.commons.lang.StringUtils.trim;
import static org.apache.commons.lang.StringUtils.trimToNull;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.annotation.PostConstruct;

import org.apache.log4j.Logger;
import org.hamcrest.Matcher;
import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.nodes.TagNode;
import org.htmlparser.util.NodeList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import de.sendorian.app.forumArchive.domain.Archive;
import de.sendorian.app.forumArchive.domain.Image;
import de.sendorian.app.forumArchive.domain.Link;
import de.sendorian.app.forumArchive.domain.Post;
import de.sendorian.app.forumArchive.domain.Topic;
import de.sendorian.app.forumArchive.domain.User;
import de.sendorian.app.forumArchive.repository.UserRepository;


/**
 * Finds a post and all data needed to initialize the post.
 * 
 * @author sendorian
 * 
 */
@Component
public abstract class PostFinder extends FinderBase<Topic, Post> {

    private static final Logger LOG = Logger.getLogger(PostFinder.class);

    protected @Value("#{config.postMatcher}") Matcher<Post> matcher;
    protected boolean recursiveSearchDisabled = false;

    protected UserRepository userRepository;
    protected @Value("#{config.baseUrl}") String baseUrl;
    protected Matcher<String> externalLinkMatcher;
    protected Matcher<String> internalLinkMatcher;


    public PostFinder() {
    }


    public PostFinder(Matcher<Post> matcher) {
        this.matcher = matcher;
    }


    /**
     * Fill linkMatcher after all attributes are injected.
     */
    @PostConstruct
    protected void init() {
        internalLinkMatcher = allOf(is(not(archive())), startsWith(baseUrl), not(containsString("mailto")));
        externalLinkMatcher = allOf(is(not(archive())), not(startsWith(baseUrl)), not(containsString("mailto")));
    }


    public List<Post> find(Topic topic) {
        return find(topic, topic.getUrl());
    }


    public List<Post> find(Topic topic, String url) {
        return doFind(topic, url, new Page(1));
    }


    private List<Post> doFind(Topic topic, String url, Page page) {
        NodeList pageContent = parse(url, null, false);
        return doFind(topic, pageContent, page);
    }


    public List<Post> find(Topic topic, NodeList pageContent) {
        return doFind(topic, pageContent, new Page(1));
    }


    private List<Post> doFind(Topic topic, NodeList pageContent, Page page) {
        List<Post> posts = new ArrayList<Post>();
        boolean dateRangeReached = false;

        LOG.debug("Found posts in topic '" + topic.getName() + "', page " + page + ":");
        NodeList postNodes = pageContent.extractAllNodesThatMatch(getPostNodeFilter(), true);
        for (Node node : postNodes.toNodeArray()) {
            TagNode row = (TagNode) node;
            String author = getAuthor(row);

            if (author != null) {
                Post post = new Post();
                post.setPostDate(getPostDate(row));
                post.setLastEditedDate(getLastEditedDate(row));
                String userName = trimToNull(unescapeHtml(author));
                if (userName != null) {
                    User user = userRepository.findByName(userName);
                    if (user == null) {
                        user = new User(userName);
                        userRepository.save(user);
                    }
                    post.setAuthor(user);
                }
                post.setMessage(trim(unescapeHtml(getMessage(row))));
                post.setFooter(trim(unescapeHtml(getFooter(row))));
                post.setImages(getImages(row));
                post.setArchives(getArchives(row));
                post.setLinks(getLinks(row));

                if (matcher.matches(post)) {
                    posts.add(post);
                    LOG.debug(post.toString());
                } else {
                    // continue with current page as the posts are ordered
                    // old -> new posts. But break recursive search after this page
                    dateRangeReached = true;
                }
            }
        }

        // break recursive search if disabled or date filter matches no more
        if (!recursiveSearchDisabled && !dateRangeReached) {
            String prevPage = getPreviousPage(pageContent);
            if (prevPage != null) {
                Page prevPageNr = new Page(page.page + 1);
                List<Post> prevPosts = doFind(topic, prevPage, prevPageNr);
                page.pageCount = prevPageNr.pageCount;
                prevPosts.addAll(posts);
                posts = prevPosts;
            }
        }

        // as this is recursive, only print count when returned to first page
        if (page.page == 1) {
            if (posts.size() > 0) {
                LOG.info("Found " + posts.size() + " matching posts in topic '" + topic.getName() + "' (on "
                        + page.pageCount + " pages).");
            } else {
                LOG.info("No posts in topic " + topic.getName() + " matching.");
            }
        }
        return posts;
    }


    public Matcher<String> getExternalLinkMatcher() {
        return externalLinkMatcher;
    }


    public Matcher<String> getInternalLinkMatcher() {
        return internalLinkMatcher;
    }


    /**
     * Returns the filter that matches the nodes that contain posts.
     */
    abstract public NodeFilter getPostNodeFilter();


    abstract protected String getAuthor(TagNode tr);


    abstract protected Date getPostDate(TagNode tr);


    abstract protected Date getLastEditedDate(TagNode tr);


    abstract protected String getMessage(TagNode tr);


    abstract protected String getFooter(TagNode tr);


    protected abstract List<Archive> getArchives(TagNode row);


    protected abstract List<Image> getImages(TagNode row);


    protected abstract List<Link> getLinks(TagNode row);


    abstract protected String getPreviousPage(NodeList tableRows);


    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }


    /**
     * Enable that the search for posts will be continued recursively through
     * the pages of the thread. (Default.)
     */
    public void enableRecursiveSearch() {
        recursiveSearchDisabled = false;
    }


    /**
     * Disable that the search for posts will be continued recursively through
     * the pages of the thread.
     */
    public void disableRecursiveSearch() {
        recursiveSearchDisabled = true;
    }
    
    
    /**
     * Represents a page of all pages in the format "Page 1 of 12".
     * 
     */
    private static final class Page {
        int page;
        int pageCount;


        Page(int page) {
            this.page = page;
            this.pageCount = page;
        }


        public String toString() {
            return "" + page;
        }
    }
}
